package weixin

import (
	"encoding/json"
	"encoding/xml"
	"errors"
	"fmt"
	"io/ioutil"
	"math/rand"
	"net/http"
	"os"
	"strconv"
	"strings"
	"time"
	"unsafe"

	"sync"

	"github.com/fumiyas/go-tty"
	"github.com/fumiyas/qrc/lib"
	"github.com/mattn/go-colorable"
	"github.com/qpliu/qrencode-go/qrencode"
)

const (
	JsLogin       = "https://login.wx.qq.com/jslogin?appid=wx782c26e4c19acffb&redirect_uri=https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage&fun=new&lang=zh_CN&_=%d"
	QrCode        = "https://login.weixin.qq.com/"
	Login         = "https://login.wx2.qq.com/cgi-bin/mmwebwx-bin/login?loginicon=true&uuid=%s&tip=%d&r=%d&_=%d"
	WebWxInit     = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxinit?r=%d&lang=zh_CN&pass_ticket=%s"
	WebWxInitData = `
{
  "BaseRequest": {
    "Uin": "%d",
    "Sid": "%s",
    "Skey": "%s",
    "DeviceID": "%s"
  }
}`
	WebSendMsg     = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxsendmsg?lang=zh_CN&pass_ticket=%s"
	WebSendMsgData = `
{
  "BaseRequest": {
    "Uin": %d,
    "Sid": "%s",
    "Skey": "%s",
    "DeviceID": "%s"
  },
  "Msg": {
    "Type": 1,
    "Content": "%s",
    "FromUserName": "%s",
    "ToUserName": "%s",
    "LocalID": "%d",
    "ClientMsgId": "%d"
  },
  "Scene": 0
}`
	WebStaNotify     = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxstatusnotify?pass_ticket=%s"
	WebStaNotifyData = `
{
  "BaseRequest": {
    "Uin": %d,
    "Sid": "%s",
    "Skey": "%s",
    "DeviceID": "%s"
  },
  "Code": 3,
  "FromUserName": "%s",
  "ToUserName": "%s",
  "ClientMsgId": %d
}`
	WebGetContact = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxgetcontact?lang=zh_CN&pass_ticket=%s&r=%d&seq=0&skey=%s"
	SyncCheck     = "https://webpush.wx2.qq.com/cgi-bin/mmwebwx-bin/synccheck?r=%d&skey=%s&sid=%s&uin=%d&deviceid=%s&synckey=%s&_=%d"
	SebWxSync     = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxsync?sid=%s&skey=%s&lang=zh_CN&pass_ticket=%s"
	SebWxSyncData = `
{
  "BaseRequest": {
    "Uin": %d,
    "Sid": "%s",
    "Skey": "%s",
    "DeviceID": "%s"
  },
  "SyncKey": %s,
  "rr": %d
}`
)

var (
	JsLoginErr      = errors.New("JsLogin error")
	WebWxInitErr    = errors.New("WebWxInit error")
	WebStaNotifyErr = errors.New("WebStaNotify error")
	WebSendMsgErr   = errors.New("WebSendMsg error")
	SebWxSyncErr    = errors.New("SebWxSync error")
	WeChatExitErr   = errors.New("exit weChat")
)

type baseResponse struct {
	Ret    int    `json:"Ret"`
	ErrMsg string `json:"ErrMsg"`
}

type xmlErr struct {
	Ret         int    `xml:"ret"`
	Message     string `xml:"message"`
	Skey        string `xml:"skey"`
	WxSid       string `xml:"wxsid"`
	WxUin       uint64 `xml:"wxuin"`
	PassTicket  string `xml:"pass_ticket"`
	IsGrayScale int    `xml:"isgrayscale"`
}

type weiXin struct {
	httpClient    *http.Client   // 普通发送消息请求使用
	httpSyncCheck *http.Client   // 检查新消息,该请求会阻塞
	myUserName    string         // 登录者的name,每次登录都不同
	setCookies    []*http.Cookie // 保存cookie
	syncKeyStr    string         // 为上面转换为字符串的数据
	contactList   contactList    // 保存获取的好友列表
	muSync        sync.Mutex     // 加锁保安全

	wxWebInit               // 返回请求的json数据
	xmlErr    `xml:"error"` // 获取登录参数
}

// 参考下面的博客以及fiddler抓包最终完成
// https://blog.csdn.net/wonxxx/article/details/51787041
func NewWeiXin() (*weiXin, error) {
	weChat := &weiXin{
		httpClient:    &http.Client{Timeout: time.Second * 35},
		httpSyncCheck: &http.Client{Timeout: time.Second * 35},
	}

	ts, rs := timeStamp(true) // 获取时间戳
	rand.Seed(ts)             // 初始化随机数种子

	byt, str, err := weChat.doHttp("GET", fmt.Sprintf(JsLogin, ts))
	if err != nil {
		return nil, err
	}

	// 返回结果异常
	if !strings.HasPrefix(str, "window.QRLogin.code = 200;") {
		return nil, JsLoginErr
	}

	uuid := findQuotes(str)[0] // 得到uuid,用于获取二维码,以及后续请求使用
	_, _, err = weChat.doHttp("GET", QrCode+"qrcode/"+uuid)
	if err != nil {
		return nil, err
	}

	// 控制台直接显示二维码
	if err = showQcode(QrCode + "l/" + uuid); err != nil {
		return nil, err
	}

	tips := 1
	for { // 408 登陆超时,201 扫描成功,200 确认登录
		ts, rs = timeStamp(false) // 获取时间戳和时间戳取反
		_, str, err = weChat.doHttp("GET", fmt.Sprintf(Login, uuid, tips, rs, ts))
		if err != nil {
			return nil, err
		}
		if strings.HasPrefix(str, "window.code=200;") {
			break
		} else if strings.HasPrefix(str, "window.code=201;") {
			tips = 0
		}
	}

	// 用doSyncCheckHttp是因为本请求resp中有set-cooke
	byt, _, err = weChat.doSyncCheckHttp("GET", findQuotes(str)[0]+"&fun=new&version=v2")
	if err != nil {
		return nil, err
	}

	// 将认证数据存入文件中
	//if err = ioutil.WriteFile("authentication.xml", byt, os.ModePerm); err != nil {
	//	return nil, err
	//}

	if err = xml.Unmarshal(byt, weChat); err != nil {
		return nil, err
	}

	byt, str, err = weChat.doHttp("POST", fmt.Sprintf(WebWxInit, rs, weChat.PassTicket),
		fmt.Sprintf(WebWxInitData, weChat.WxUin, weChat.WxSid, weChat.Skey, deviceID()))
	if err != nil {
		return nil, err
	}

	if err = json.Unmarshal(byt, weChat); err != nil {
		return nil, err
	}

	weChat.syncKeyStr = "" // 清空synckey
	for i, v := range weChat.SyncKey.List {
		if i > 0 {
			weChat.syncKeyStr += "|"
		}
		weChat.syncKeyStr += fmt.Sprintf("%d_%d", v.Key, v.Val)
	}

	if weChat.BaseResp.Ret != 0 { // 获取信息失败
		return nil, WebWxInitErr
	}
	weChat.myUserName = weChat.UserInfo.UserName // 记录登录者的名字

	ts, _ = timeStamp(true) // 获取时间戳
	byt, _, err = weChat.doHttp("POST", WebStaNotify,
		fmt.Sprintf(WebStaNotifyData, weChat.WxUin, weChat.WxSid, weChat.Skey,
			deviceID(), weChat.myUserName, weChat.myUserName, ts))
	if err != nil {
		return nil, err
	}

	if err = json.Unmarshal(byt, weChat); err != nil {
		return nil, err
	}

	if weChat.BaseResp.Ret != 0 { // 获取信息失败
		return nil, WebStaNotifyErr
	}

	// 将好友列表数据存入内存
	if err = weChat.getContactList(); err != nil {
		return nil, err
	}

	go func() {
		for {
			// 如果捕捉到被人退出登录,则需要特殊处理
			if err = weChat.syncCheck(); err == WeChatExitErr {
				fmt.Println(err)
				os.Exit(0) // 被退出了,整个程序都退了吧
			}
		}
	}()

	return weChat, nil
}

// 发送微信消息
func (c *weiXin) SendMsg(name, msg string) error {
	var haveName string
	name = strings.TrimSpace(name) // 去除左右两侧空白字符
	for _, v := range c.contactList.MemberList {
		// 只要昵称或者备注名称包含接收消息的名字就发送到临时用户名中
		if strings.Contains(v.NickName, name) ||
			strings.Contains(v.RemarkName, name) {
			haveName = v.UserName
			break
		}
	}

	if haveName == "" { // 好友列表没有这个人或者群
		return fmt.Errorf("can not find name :[%s]", name)
	}

	ts, _ := timeStamp(true)       // 获取时间戳
	ts = ts<<4 + rand.Int63n(9999) // 时间戳左移4位随后补上4位随机数
	byt, _, err := c.doHttp("POST", fmt.Sprintf(WebSendMsg, c.PassTicket),
		fmt.Sprintf(WebSendMsgData, c.WxUin, c.WxSid, c.Skey, deviceID(),
			msg, c.myUserName, haveName, ts, ts))

	if err = json.Unmarshal(byt, c); err != nil {
		return err
	}

	if c.BaseResp.Ret != 0 {
		return WebSendMsgErr
	}

	fmt.Printf("SendMsg to[%s],msg:%s\n", name, msg)
	return nil
}

// 得到好友列表
func (c *weiXin) getContactList() error {
	ts, _ := timeStamp(true) // 获取时间戳
	byt, _, err := c.doHttp("GET", fmt.Sprintf(WebGetContact, ts, c.PassTicket, c.Skey))
	if err != nil {
		return err
	}

	return json.Unmarshal(byt, &c.contactList)
}

// 检查是否有更新,如果有则处理
// 本请求为阻塞,和普通请求分开调用
func (c *weiXin) syncCheck() error {
	ts, rs := timeStamp(false) // 获取时间戳和时间戳取反

	byt, str, err := c.doSyncCheckHttp("GET",
		fmt.Sprintf(SyncCheck, ts, c.Skey, c.WxSid, c.WxUin, deviceID(),
			c.syncKeyStr, ts+rand.Int63n(999)))
	if err != nil {
		return err
	}

	syncCheckRet := findQuotes(str)
	if syncCheckRet[0] != "0" {
		return WeChatExitErr // retcode不为0则异常,退出登录吧
	}

	switch syncCheckRet[1] {
	//case "0": // 这个消息是正常不处理
	case "2": // 有新的消息要处理
		fmt.Println("SyncCheck :", str)
		if byt, err = json.Marshal(c.SyncKey); err != nil {
			return err
		}

		byt, _, err = c.doSyncCheckHttp("POST",
			fmt.Sprintf(SebWxSync, c.WxSid, c.Skey, c.PassTicket),
			fmt.Sprintf(SebWxSyncData, c.WxUin, c.WxSid, c.Skey, deviceID(), byt, rs))
		if err != nil {
			return err
		}

		if err = json.Unmarshal(byt, c); err != nil {
			return err
		}

		if c.BaseResp.Ret != 0 {
			return SebWxSyncErr
		}

		c.syncKeyStr = "" // 清空synckey
		for i, v := range c.SyncKey.List {
			if i > 0 {
				c.syncKeyStr += "|"
			}
			c.syncKeyStr += fmt.Sprintf("%d_%d", v.Key, v.Val)
		}
	case "7": // 其他地方扫码登录导致这里被踢下线了
		return WeChatExitErr
	}

	return nil
}

/**
arg[0] == "GET"  时,arg[1]为url
arg[0] == "POST" 时,arg[1]为url,arg[2]为data
**/
func (c *weiXin) doHttp(arg ...string) (reByte []byte, reStr string, err error) {
	c.muSync.Lock() // 加锁保安全
	defer c.muSync.Unlock()

	var req *http.Request
	if arg[0] == "GET" {
		req, err = http.NewRequest("GET", arg[1], nil)
	} else {
		req, err = http.NewRequest("POST", arg[1], strings.NewReader(arg[2]))
	}
	if err != nil { // 创建post的req对象
		return
	}

	for _, cookie := range c.setCookies {
		req.AddCookie(cookie) // 设置cookie
	}

	var resp *http.Response
	resp, err = c.httpClient.Do(req) // 复用HttpClient对象
	if err != nil {
		return
	}
	defer resp.Body.Close() // 最终关闭连接

	reByte, err = ioutil.ReadAll(resp.Body)
	if err == nil { // []byte和string共用内存
		reStr = *(*string)(unsafe.Pointer(&reByte))
	}
	return
}

/**
arg[0] == "GET"  时,arg[1]为url
arg[0] == "POST" 时,arg[1]为url,arg[2]为data
该方法和上面方法的区别在于resp中会有set-cooke
且在运行中只能由SyncCheck和SebWxSync这两个请求调用,其他请求用上面那个
**/
func (c *weiXin) doSyncCheckHttp(arg ...string) (reByte []byte, reStr string, err error) {
	var req *http.Request
	if arg[0] == "GET" {
		req, err = http.NewRequest("GET", arg[1], nil)
	} else {
		req, err = http.NewRequest("POST", arg[1], strings.NewReader(arg[2]))
	}
	if err != nil { // 创建post的req对象
		return
	}

	for _, cookie := range c.setCookies {
		req.AddCookie(cookie) // 设置cookie
	}

	var resp *http.Response
	resp, err = c.httpSyncCheck.Do(req) // 复用HttpClient对象
	if err != nil {
		return
	}
	defer resp.Body.Close() // 最终关闭连接

	if tmp := resp.Cookies(); len(tmp) > 0 {
		c.setCookies = tmp // 当Set-Cookie有数据
	}

	reByte, err = ioutil.ReadAll(resp.Body)
	if err == nil { // []byte和string共用内存
		reStr = *(*string)(unsafe.Pointer(&reByte))
	}
	return
}

/**
only == true  只返回当前时间戳
only == false 返回当前时间戳和时间戳取反

在js代码中为:
var ts = new Date()
var rs = ~ts
**/
func timeStamp(only bool) (ts int64, rs int64) {
	ts = time.Now().UnixNano() / 1e6
	if only { // 只需要时间戳
		return
	}

	retStr := ""
	binary := fmt.Sprintf("%064b", ts)
	for i := 32; i < 64; i++ {
		if binary[i] == 48 {
			retStr += "1"
		} else {
			retStr += "0"
		}
	}

	// 将结果的二进制字符串转换为int64的10进制
	rs, _ = strconv.ParseInt(retStr, 2, 64)
	return
}

/**
js代码:
return "e" + ("" + Math.random().toFixed(15)).substring(2, 17)
**/
func deviceID() string {
	return fmt.Sprintf("e%015d", rand.Int63n(999999999999999))
}

/**
屏幕显示要扫描的二维码
**/
func showQcode(msg string) error {
	grid, err := qrencode.Encode(msg, qrencode.ECLevelM)
	if err != nil {
		return err
	}

	da1, err := tty.GetDeviceAttributes1(os.Stdout)
	if err == nil && da1[tty.DA1_SIXEL] {
		qrc.PrintSixel(os.Stdout, grid, false)
	} else {
		stdout := colorable.NewColorableStdout()
		qrc.PrintAA(stdout, grid, false)
	}
	return nil
}

/**
找出字符串中2个引号之间的数据
**/
func findQuotes(data string) []string {
	var idx []int
	for i, v := range data {
		if v == 34 { // 找到所有{"}的位置
			idx = append(idx, i)
		}
	}

	var (
		idxLen = len(idx)
		ret    []string
	)
	if idxLen >= 2 && idxLen%2 == 0 { // 至少2个引号,以及成对出现才记录
		for i := 0; i < idxLen; i += 2 {
			if idx[i+1]-idx[i] > 1 { // 两个引号中间有数据才记录
				ret = append(ret, data[idx[i]+1:idx[i+1]])
			}
		}
	}

	return ret
}
